package org.gluedom.data;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;

import com.google.common.collect.ImmutableList;

public class Concept implements Immutable, Identifiable, Ownable,
		PropertyContainer {
	private final Identifier identifier;
	private final Identifier ownerId;
	private final List<Attribute> attributes;
	private final List<ConceptRelation> relations;
	private final List<Property> properties;

	private volatile int hash = 0;

	private Concept(Builder builder) {
		identifier = builder.identifier.identifier;
		ownerId = builder.ownerId.ownerId;
		attributes = ImmutableList.copyOf(builder.attributes);
		relations = ImmutableList.copyOf(builder.relations);
		properties = ImmutableList.copyOf(builder.properties.properties);
	}

	@Override
	public Identifier getIdentifier() {
		return identifier;
	}

	@Override
	public Identifier getOwnerId() {
		return ownerId;
	}

	public Iterable<Attribute> getAttributes() {
		return attributes;
	}

	public Attribute getAttribute(final Identifier identifier) {
		return (Attribute) FunctionalHashedComparator.findListEntry(
				new Identifiable() {
					@Override
					public Identifier getIdentifier() {
						return identifier;
					}
				}, attributes, Identifier.IDENTIFYHASH_COMPARATOR);
	}

	public Iterable<ConceptRelation> getRelations() {
		return relations;
	}

	public Iterable<ConceptRelation> getOutRelations(Identifier identifier) {
		String hashedId = ConceptRelation.generateHashedId(identifier,
				this.identifier, null);
		int[] section = binarySectionSearch(hashedId);
		return new RelationIterable(section, null);
	}

	public Iterable<ConceptRelation> getInRelations(Identifier identifier) {
		String hashedId = ConceptRelation.generateHashedId(identifier, null,
				null);
		int[] section = binarySectionSearch(hashedId);
		hashedId = ConceptRelation.generateHashedId(identifier,
				this.identifier, null);
		int[] except = binarySectionSearch(hashedId);
		return new RelationIterable(section, except);
	}

	private class RelationIterable implements Iterable<ConceptRelation> {
		private final int[] section;
		private final int[] except;

		public RelationIterable(int[] section, int[] except) {
			assert section != null;
			this.section = section;
			if (except != null)
				this.except = except;
			else
				this.except = new int[] { relations.size(), relations.size() };
		}

		@Override
		public Iterator<ConceptRelation> iterator() {
			return new Iterator<ConceptRelation>() {
				private int i = (section[0] < except[0] ? section[0]
						: except[1]);

				@Override
				public boolean hasNext() {
					return i < section[1];
				}

				@Override
				public ConceptRelation next() {
					if (i < section[1]) {
						int r = i;
						i++;
						if (i >= except[0] && i < except[1])
							i = except[1];
						return relations.get(r);
					} else
						throw new NoSuchElementException();
				}

				@Override
				public void remove() {
					throw new UnsupportedOperationException();
				}
			};
		}
	}

	private int[] binarySectionSearch(String hashedId) {
		int[] result = new int[] { 0, 0 };
		int low = 0;
		int high = relations.size();
		int i, a = -1;
		while (low < high) {
			i = (high + low) / 2;
			String relHash = relations.get(i).getHashedId();
			a = (relHash.substring(0, Math.min(relHash.length(), hashedId
					.length()))).compareTo(hashedId);
			if (a < 0)
				low = i;
			else if (a >= 0)
				high = i;
		}
		result[0] = low;
		low = 0;
		high = relations.size();
		while (low < high) {
			i = (high + low) / 2;
			String relHash = relations.get(i).getHashedId();
			a = (relHash.substring(0, Math.min(relHash.length(), hashedId
					.length()))).compareTo(hashedId);
			if (a <= 0 && low != i)
				low = i;
			else if (a >= 0)
				high = i;
		}
		result[1] = low + (a == 0 ? 1 : 0);
		return result;
	}

	@Override
	public Iterable<Property> getProperties() {
		return properties;
	}

	@Override
	public String getProperty(String name) {
		return Property.findProperty(name, properties);
	}

	public static class Builder {
		private final Identifiable.Builder identifier;
		private final Ownable.Builder ownerId;
		private final Set<Attribute> attributes = new TreeSet<Attribute>(
				Identifier.IDENTIFYHASH_COMPARATOR);
		private final Set<ConceptRelation> relations = new TreeSet<ConceptRelation>(
				ConceptRelation.RELATION_COMPARATOR);
		private final PropertyContainer.Builder properties = new PropertyContainer.Builder();

		public Builder(Concept concept) {
			identifier = new Identifiable.Builder(concept.identifier);
			ownerId = new Ownable.Builder(concept.ownerId);
			attributes.addAll(concept.attributes);
			relations.addAll(concept.relations);
			properties.properties(concept.properties);
		}

		public Builder(Identifier identifier, Identifier ownerId) {
			this.identifier = new Identifiable.Builder(checkNotNull(identifier));
			this.ownerId = new Ownable.Builder(checkNotNull(ownerId));
		}

		public Builder identifier(Identifier identifier) {
			this.identifier.identifier(checkNotNull(identifier));
			return this;
		}

		public Builder ownerId(Identifier ownerId) {
			this.ownerId.ownerId(checkNotNull(ownerId));
			return this;
		}

		public Builder attribute(Attribute attribute) {
			attributes.remove(attribute);
			attributes.add(checkNotNull(attribute));
			return this;
		}

		public Builder attributes(Collection<Attribute> attributes) {
			checkArgument(!attributes.contains(null),
					"attributes collection contains null value");
			this.attributes.removeAll(attributes);
			this.attributes.addAll(attributes);
			return this;
		}

		public Builder relation(ConceptRelation relation) {
			relations.remove(relation);
			relations.add(checkNotNull(relation));
			return this;
		}

		public Builder relations(Collection<ConceptRelation> relations) {
			checkArgument(!relations.contains(null),
					"relations collection contains null value");
			this.relations.removeAll(relations);
			this.relations.addAll(relations);
			return this;
		}

		public Builder property(String name, String value) {
			properties.property(name, value);
			return this;
		}

		public Builder property(Property property) {
			properties.property(checkNotNull(property));
			return this;
		}

		public Builder properties(Collection<Property> properties) {
			this.properties.properties(properties);
			return this;
		}

		public Concept build() {
			return new Concept(this);
		}
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (!(obj instanceof Concept))
			return false;
		Concept other = (Concept) obj;
		if (hashCode() != other.hashCode())
			return false;
		if (!identifier.equals(other.identifier))
			return false;
		if (!ownerId.equals(other.ownerId))
			return false;
		if (!attributes.equals(other.attributes))
			return false;
		if (!relations.equals(other.relations))
			return false;
		if (!properties.equals(other.properties))
			return false;
		return true;
	}

	@Override
	public int hashCode() {
		if (hash == 0) {
			int result = 572481;
			result = 31 * result + identifier.hashCode();
			result = 31 * result + ownerId.hashCode();
			result = 31 * result + attributes.hashCode();
			result = 31 * result + relations.hashCode();
			result = 31 * result + properties.hashCode();
			hash = result;
		}
		return hash;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append(identifier);
		sb.append(" {\n");
		sb.append("  $owner `");
		sb.append(ownerId);
		sb.append("`\n");
		for (Attribute a : attributes) {
			sb.append(a);
			sb.append("\n");
		}
		sb.append(properties);
		sb.append("\n");
		for (ConceptRelation cr : relations) {
			sb.append(cr);
			sb.append("\n");
		}
		sb.append("}\n");
		return sb.toString();
	}
}
